/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of SimpleEL.
 * 
 * SimpleEL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * SimpleEL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with SimpleEL. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.simpleel.internal;

import java.lang.management.*;
import java.util.*;
import java.util.concurrent.*;
import ch.msoftch.internal.*;

/**
 * Benchmark for {@link ICache}.
 * 
 * @author M. Hautle
 */
public class CacheBenchmark {
    /** The test types. */
    private enum Type {
        /** Only reading test. */
        READ,
        /** Stress test with concurrent reading and writing. */
        STRESS,
        /** Test with concurrent reading and writing but with more reads than writes. */
        RATIO
    }

    /** The number of test elements. */
    private static final int NUMBERS = 100000;

    /** The number of concurrent threads. */
    private static final int THREADS = 10;

    /** The number of rounds. */
    private static final int ROUNDS = 10;

    /** The type of the test to run. */
    private static final Type type = Type.RATIO;

    /** The ratio between reader and writer threads to use for {@link Type#RATIO} tests. */
    private static final int RATIO = 4;

    /** The expected values. */
    private Long[] values;

    /** The keys for the {@link #values}. */
    private Integer[] keys;

    /** The cache to test. */
    private ICache<Integer, Long> cache;

    /**
     * Sets up the cache.
     */
    private void setupCache() {
        // cache = new ConcurrentMapCache<Integer, Long>();
        cache = new LockedCache<Integer, Long>();
        // cache = new ExperimentalCache<Integer, Long>();
        // cache = new SynchronizedCache<Integer, Long>();
        for (int i = 0; i < values.length; i++)
            cache.put(keys[i], values[i]);
    }

    /**
     * Main for the benchmark.
     * 
     * @param args Not used
     * @throws Exception If something went wrong
     */
    public static void main(String[] args) throws Exception {
        final CacheBenchmark b = new CacheBenchmark();
        b.setUp();
        b.runTests(ROUNDS * 5);
        System.err.println("Real test: " + b.cache.getClass().getSimpleName() + " " + type.name() + " " + THREADS + " theads");
        b.runTests(ROUNDS);
        System.err.println("Memory:");
        MemoryMXBean mem = ManagementFactory.getMemoryMXBean();
        System.out.println("Heap: " + mem.getHeapMemoryUsage().getUsed());
        System.out.println("Non Heap: " + mem.getNonHeapMemoryUsage().getUsed());
        for (GarbageCollectorMXBean g : ManagementFactory.getGarbageCollectorMXBeans()) {
            System.out.println(g.getName() + ": " + g.getCollectionCount() + " " + g.getCollectionTime() + "ms");
        }
    }

    /**
     * Set up the cache.
     */
    private void setUp() {
        values = new Long[NUMBERS];
        keys = new Integer[NUMBERS];
        for (int i = 0; i < values.length; i++) {
            final int v = i * 2;
            values[i] = new Long(v);
            keys[i] = new Integer(v);
        }
    }

    /**
     * Runs the the test for the specified number of rounds.
     * 
     * @param rounds The number of rounds
     * @throws Exception If something went wrong
     */
    private void runTests(int rounds) throws Exception {
        long[] res = new long[rounds];
        for (int i = 0; i < rounds; i++)
            res[i] = runTest();
        System.out.println("Average: " + getAvg(res) + " ms for " + NUMBERS + " reading calls");
    }

    /**
     * Runs a test round.
     * 
     * @return The run time in ms
     * @throws Exception If someting went wrong
     */
    private long runTest() throws Exception {
        setupCache();
        long t = System.currentTimeMillis();
        switch (type) {
            case STRESS:
                stressTest();
                break;
            case READ:
                concurrentReadTest();
                break;
            case RATIO:
                ratioTest();
        }
        return System.currentTimeMillis() - t;
    }

    /**
     * Test with only reading threads.
     * 
     * @throws Exception If something went wrong
     */
    public void concurrentReadTest() throws Exception {
        final CountDownLatch b = new CountDownLatch(THREADS);
        final Checker[] checkers = new Checker[THREADS];
        for (int i = 0; i < THREADS; i++) {
            checkers[i] = new Checker(b);
            checkers[i].start();
        }
        b.await();
        for (Checker c : checkers)
            if (!c.getFailed().isEmpty())
                throw new RuntimeException("Error");
    }

    /**
     * Test with the same number of reading and writing threads.
     * 
     * @throws Exception If something went wrong
     */
    public void stressTest() throws Exception {
        final CountDownLatch writers = new CountDownLatch(THREADS);
        final CountDownLatch readers = new CountDownLatch(THREADS);
        final Checker[] checkers = new Checker[THREADS];
        final Writer[] writes = new Writer[THREADS];
        // start the writer threads
        for (int i = 0, step = (NUMBERS * 2) / THREADS; i < THREADS; i++)
            writes[i] = new Writer(i * step, (i + 1) * step, writers);
        // start the reader threads
        for (int i = 0; i < THREADS; i++) {
            checkers[i] = new Checker(readers);
            checkers[i].start();
            writes[i].start();
        }
        // wait until all readers were done
        readers.await();
        writers.await();
        for (Checker c : checkers)
            if (!c.getFailed().isEmpty())
                throw new RuntimeException("Error");
    }

    /**
     * Test with reading threads and writing threads with a ration of {@link #RATIO} (writing = reading / {@link #RATIO}).
     * 
     * @throws Exception If something went wrong
     */
    public void ratioTest() throws Exception {
        final CountDownLatch writers = new CountDownLatch(THREADS / RATIO);
        final CountDownLatch readers = new CountDownLatch(THREADS);
        final Checker[] checkers = new Checker[THREADS];
        final Writer[] writes = new Writer[THREADS / RATIO];
        // start the writer threads
        for (int i = 0, cnt = THREADS / RATIO, step = (NUMBERS * 2) / THREADS; i < cnt; i++)
            writes[i] = new Writer(i * step, (i + 1) * step, writers);
        // start the reader threads
        for (int i = 0; i < THREADS; i++) {
            checkers[i] = new Checker(readers);
            checkers[i].start();
            if (i < writes.length)
                writes[i].start();
        }
        // wait until all readers were done
        readers.await();
        writers.await();
        for (Checker c : checkers)
            if (!c.getFailed().isEmpty())
                throw new RuntimeException("Error");
    }

    /**
     * Computes the average time out of the passed ones.
     * 
     * @param values Timing values
     * @return The average
     */
    private static double getAvg(long[] values) {
        double sum = 0;
        for (long l : values)
            sum += l;
        return sum / values.length;
    }

    /**
     * A writer thread.<br>
     * Writes from the given start value to the end value in two value steps.
     * 
     * @author M. Hautle
     */
    private class Writer extends Thread {
        /** The count down of the writer threads. */
        private final CountDownLatch countdown;

        /** The start value. */
        private final int start;

        /** The end value. */
        private final int end;

        /**
         * Default constructor.
         * 
         * @param start The start value
         * @param end The end value
         * @param countdown The writer count down
         */
        public Writer(int start, int end, CountDownLatch countdown) {
            this.start = start;
            this.end = end;
            this.countdown = countdown;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void run() {
            for (int i = start + 1; i < end; i += 2) {
                cache.put(new Integer(i), new Long(i - 1));
                Thread.yield();
            }
            countdown.countDown();
        }
    }

    /**
     * The checker thread.
     * 
     * @author M. Hautle
     */
    private class Checker extends Thread {
        /** The count down latch used by the test. */
        private final CountDownLatch countdown;

        /** List holding the failed keys. */
        private final List<Integer> failed = new ArrayList<Integer>();

        /**
         * Default constructor.
         * 
         * @param countdown The count down to inform
         */
        public Checker(CountDownLatch countdown) {
            this.countdown = countdown;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void run() {
            for (int i = 0; i < keys.length; i++) {
                if (values[i] != cache.get(keys[i]))
                    failed.add(keys[i]);
                Thread.yield();
            }
            countdown.countDown();
        }

        /**
         * @return Returns the failed.
         */
        public List<Integer> getFailed() {
            return failed;
        }
    }
}
